package org.andengine.extension.svg.adt;

import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Style;
import android.graphics.Path;
import android.graphics.RectF;
import android.graphics.Shader;

import org.andengine.extension.svg.adt.SVGGradient.SVGGradientStop;
import org.andengine.extension.svg.adt.filter.SVGFilter;
import org.andengine.extension.svg.adt.filter.element.SVGFilterElementGaussianBlur;
import org.andengine.extension.svg.exception.SVGParseException;
import org.andengine.extension.svg.util.SAXHelper;
import org.andengine.extension.svg.util.SVGParserUtils;
import org.andengine.extension.svg.util.constants.ColorUtils;
import org.andengine.extension.svg.util.constants.ISVGConstants;
import org.xml.sax.Attributes;

import java.util.HashMap;


/**
 * (c) 2010 Nicolas Gramlich
 * (c) 2011 Zynga Inc.
 *
 * @author Nicolas Gramlich
 * @since 22:01:39 - 23.05.2011
 */
public class SVGPaint implements ISVGConstants {
    // ===========================================================
    // Constants
    // ===========================================================

    // ===========================================================
    // Fields
    // ===========================================================

    private final Paint mPaint = new Paint();

    private final ISVGColorMapper mSVGColorMapper;

    /**
     * Multi purpose dummy rectangle.
     */
    private final RectF mRect = new RectF();
    private final RectF mComputedBounds = new RectF(Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY, Float.NEGATIVE_INFINITY, Float.NEGATIVE_INFINITY);

    private final HashMap<String, SVGGradient> mSVGGradientMap = new HashMap<String, SVGGradient>();
    private final HashMap<String, SVGFilter> mSVGFilterMap = new HashMap<String, SVGFilter>();

    // ===========================================================
    // Constructors
    // ===========================================================

    public SVGPaint(final ISVGColorMapper pSVGColorMapper) {
        this.mSVGColorMapper = pSVGColorMapper;
    }

    // ===========================================================
    // Getter & Setter
    // ===========================================================

    public Paint getPaint() {
        return this.mPaint;
    }

    public RectF getComputedBounds() {
        return this.mComputedBounds;
    }

    // ===========================================================
    // Methods for/from SuperClass/Interfaces
    // ===========================================================

    // ===========================================================
    // Methods
    // ===========================================================

    public void resetPaint(final Style pStyle) {
        this.mPaint.reset();
        this.mPaint.setAntiAlias(true); // TODO AntiAliasing could be made optional through some SVGOptions object.
        this.mPaint.setStyle(pStyle);
    }

    /**
     * TODO Would it be better/cleaner to throw a SVGParseException when sth could not be parsed instead of simply returning false?
     */
    public boolean setFill(final SVGProperties pSVGProperties) {
        if (this.isDisplayNone(pSVGProperties) || this.isFillNone(pSVGProperties)) {
            return false;
        }

        this.resetPaint(Paint.Style.FILL);

        final String fillProperty = pSVGProperties.getStringProperty(ATTRIBUTE_FILL);
        if (fillProperty == null) {
            if (pSVGProperties.getStringProperty(ATTRIBUTE_STROKE) == null) {
                /* Default is black fill. */
                this.mPaint.setColor(0xFF000000); // TODO Respect color mapping?
                return true;
            } else {
                return false;
            }
        } else {
            return this.applyPaintProperties(pSVGProperties, true);
        }
    }

    public boolean setStroke(final SVGProperties pSVGProperties) {
        if (this.isDisplayNone(pSVGProperties) || this.isStrokeNone(pSVGProperties)) {
            return false;
        }

        this.resetPaint(Paint.Style.STROKE);

        return this.applyPaintProperties(pSVGProperties, false);
    }

    private boolean isDisplayNone(final SVGProperties pSVGProperties) {
        return VALUE_NONE.equals(pSVGProperties.getStringProperty(ATTRIBUTE_DISPLAY));
    }

    private boolean isFillNone(final SVGProperties pSVGProperties) {
        return VALUE_NONE.equals(pSVGProperties.getStringProperty(ATTRIBUTE_FILL));
    }

    private boolean isStrokeNone(final SVGProperties pSVGProperties) {
        return VALUE_NONE.equals(pSVGProperties.getStringProperty(ATTRIBUTE_STROKE));
    }

    public boolean applyPaintProperties(final SVGProperties pSVGProperties, final boolean pModeFill) {
        if (this.setColorProperties(pSVGProperties, pModeFill)) {
            if (pModeFill) {
                return this.applyFillProperties(pSVGProperties);
            } else {
                return this.applyStrokeProperties(pSVGProperties);
            }
        } else {
            return false;
        }
    }

    private boolean setColorProperties(final SVGProperties pSVGProperties, final boolean pModeFill) { // TODO throw SVGParseException
        final String colorProperty = pSVGProperties.getStringProperty(pModeFill ? ATTRIBUTE_FILL : ATTRIBUTE_STROKE);
        if (colorProperty == null) {
            return false;
        }

        final String filterProperty = pSVGProperties.getStringProperty(ATTRIBUTE_FILTER);
        if (filterProperty != null) {
            if (SVGProperties.isURLProperty(filterProperty)) {
                final String filterID = SVGParserUtils.extractIDFromURLProperty(filterProperty);

                this.getFilter(filterID).applyFilterElements(this.mPaint);
            } else {
                return false;
            }
        }

        if (SVGProperties.isURLProperty(colorProperty)) {
            final String gradientID = SVGParserUtils.extractIDFromURLProperty(colorProperty);

            this.mPaint.setShader(this.getGradientShader(gradientID));
            return true;
        } else {
            final Integer color = this.parseColor(colorProperty);
            if (color != null) {
                this.applyColor(pSVGProperties, color, pModeFill);
                return true;
            } else {
                return false;
            }
        }
    }

    private boolean applyFillProperties(final SVGProperties pSVGProperties) {
        return true;
    }

    private boolean applyStrokeProperties(final SVGProperties pSVGProperties) {
        final Float width = pSVGProperties.getFloatProperty(ATTRIBUTE_STROKE_WIDTH);
        if (width != null) {
            this.mPaint.setStrokeWidth(width);
        }
        final String linecap = pSVGProperties.getStringProperty(ATTRIBUTE_STROKE_LINECAP);
        if (ATTRIBUTE_STROKE_LINECAP_VALUE_ROUND.equals(linecap)) {
            this.mPaint.setStrokeCap(Paint.Cap.ROUND);
        } else if (ATTRIBUTE_STROKE_LINECAP_VALUE_SQUARE.equals(linecap)) {
            this.mPaint.setStrokeCap(Paint.Cap.SQUARE);
        } else if (ATTRIBUTE_STROKE_LINECAP_VALUE_BUTT.equals(linecap)) {
            this.mPaint.setStrokeCap(Paint.Cap.BUTT);
        }
        final String linejoin = pSVGProperties.getStringProperty(ATTRIBUTE_STROKE_LINEJOIN_VALUE_);
        if (ATTRIBUTE_STROKE_LINEJOIN_VALUE_MITER.equals(linejoin)) {
            this.mPaint.setStrokeJoin(Paint.Join.MITER);
        } else if (ATTRIBUTE_STROKE_LINEJOIN_VALUE_ROUND.equals(linejoin)) {
            this.mPaint.setStrokeJoin(Paint.Join.ROUND);
        } else if (ATTRIBUTE_STROKE_LINEJOIN_VALUE_BEVEL.equals(linejoin)) {
            this.mPaint.setStrokeJoin(Paint.Join.BEVEL);
        }
        return true;
    }

    private void applyColor(final SVGProperties pSVGProperties, final Integer pColor, final boolean pModeFill) {
        final int c = (ColorUtils.COLOR_MASK_32BIT_ARGB_RGB & pColor) | ColorUtils.COLOR_MASK_32BIT_ARGB_ALPHA;
        this.mPaint.setColor(c);
        this.mPaint.setAlpha(SVGPaint.parseAlpha(pSVGProperties, pModeFill));
    }

    private static int parseAlpha(final SVGProperties pSVGProperties, final boolean pModeFill) {
        Float opacity = pSVGProperties.getFloatProperty(ATTRIBUTE_OPACITY);
        if (opacity == null) {
            opacity = pSVGProperties.getFloatProperty(pModeFill ? ATTRIBUTE_FILL_OPACITY : ATTRIBUTE_STROKE_OPACITY);
        }
        if (opacity == null) {
            return 255;
        } else {
            return (int) (255 * opacity);
        }
    }

    public void ensureComputedBoundsInclude(final float pX, final float pY) {
        if (pX < this.mComputedBounds.left) {
            this.mComputedBounds.left = pX;
        }
        if (pX > this.mComputedBounds.right) {
            this.mComputedBounds.right = pX;
        }
        if (pY < this.mComputedBounds.top) {
            this.mComputedBounds.top = pY;
        }
        if (pY > this.mComputedBounds.bottom) {
            this.mComputedBounds.bottom = pY;
        }
    }

    public void ensureComputedBoundsInclude(final float pX, final float pY, final float pWidth, final float pHeight) {
        this.ensureComputedBoundsInclude(pX, pY);
        this.ensureComputedBoundsInclude(pX + pWidth, pY + pHeight);
    }

    public void ensureComputedBoundsInclude(final Path pPath) {
        pPath.computeBounds(this.mRect, false);
        this.ensureComputedBoundsInclude(this.mRect.left, this.mRect.top);
        this.ensureComputedBoundsInclude(this.mRect.right, this.mRect.bottom);
    }

    // ===========================================================
    // Methods for Colors
    // ===========================================================

    private Integer parseColor(final String pString, final Integer pDefault) {
        final Integer color = this.parseColor(pString);
        if (color == null) {
            return this.applySVGColorMapper(pDefault);
        } else {
            return color;
        }
    }

    private Integer parseColor(final String pString) {
        /* TODO Test if explicit pattern matching is faster:
         *
		 * RGB:		/^rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$/
		 * #RRGGBB:	/^(\w{2})(\w{2})(\w{2})$/
		 * #RGB:	/^(\w{1})(\w{1})(\w{1})$/
		 */

        final Integer parsedColor;
        if (pString == null) {
            parsedColor = null;
        } else if (SVGProperties.isHexProperty(pString)) {
            parsedColor = SVGParserUtils.extractColorFromHexProperty(pString);
        } else if (SVGProperties.isRGBProperty(pString)) {
            parsedColor = SVGParserUtils.extractColorFromRGBProperty(pString);
        } else {
            final Integer colorByName = ColorUtils.getColorByName(pString.trim());
            if (colorByName != null) {
                parsedColor = colorByName;
            } else {
                parsedColor = SVGParserUtils.extraColorIntegerProperty(pString);
            }
        }
        return this.applySVGColorMapper(parsedColor);
    }

    private Integer applySVGColorMapper(final Integer pColor) {
        if (this.mSVGColorMapper == null) {
            return pColor;
        } else {
            return this.mSVGColorMapper.mapColor(pColor);
        }
    }

    // ===========================================================
    // Methods for Gradients
    // ===========================================================

    public SVGFilter parseFilter(final Attributes pAttributes) {
        final String id = SAXHelper.getStringAttribute(pAttributes, ATTRIBUTE_ID);
        if (id == null) {
            return null;
        }

        final SVGFilter svgFilter = new SVGFilter(id, pAttributes);
        this.mSVGFilterMap.put(id, svgFilter);
        return svgFilter;
    }

    public SVGGradient parseGradient(final Attributes pAttributes, final boolean pLinear) {
        final String id = SAXHelper.getStringAttribute(pAttributes, ATTRIBUTE_ID);
        if (id == null) {
            return null;
        }

        final SVGGradient svgGradient = new SVGGradient(id, pLinear, pAttributes);
        this.mSVGGradientMap.put(id, svgGradient);
        return svgGradient;
    }

    public SVGGradientStop parseGradientStop(final SVGProperties pSVGProperties) {
        final float offset = pSVGProperties.getFloatProperty(ATTRIBUTE_OFFSET, 0f);
        final String stopColor = pSVGProperties.getStringProperty(ATTRIBUTE_STOP_COLOR);
        final int rgb = this.parseColor(stopColor.trim(), Color.BLACK);
        final int alpha = this.parseGradientStopAlpha(pSVGProperties);
        return new SVGGradientStop(offset, alpha | rgb);
    }

    private int parseGradientStopAlpha(final SVGProperties pSVGProperties) {
        final String opacityStyle = pSVGProperties.getStringProperty(ATTRIBUTE_STOP_OPACITY);
        if (opacityStyle != null) {
            final float alpha = Float.parseFloat(opacityStyle);
            final int alphaInt = Math.round(255 * alpha);
            return (alphaInt << 24);
        } else {
            return ColorUtils.COLOR_MASK_32BIT_ARGB_ALPHA;
        }
    }

    private Shader getGradientShader(final String pGradientShaderID) {
        final SVGGradient svgGradient = this.mSVGGradientMap.get(pGradientShaderID);
        if (svgGradient == null) {
            throw new SVGParseException("No SVGGradient found for id: '" + pGradientShaderID + "'.");
        } else {
            final Shader gradientShader = svgGradient.getShader();
            if (gradientShader != null) {
                return gradientShader;
            } else {
                svgGradient.ensureHrefResolved(this.mSVGGradientMap);
                return svgGradient.createShader();
            }
        }
    }

    // ===========================================================
    // Methods for Filters
    // ===========================================================

    private SVGFilter getFilter(final String pSVGFilterID) {
        final SVGFilter svgFilter = this.mSVGFilterMap.get(pSVGFilterID);
        if (svgFilter == null) {
            return null; // TODO Better a SVGParseException here?
        } else {
            svgFilter.ensureHrefResolved(this.mSVGFilterMap);
            return svgFilter;
        }
    }

    public SVGFilterElementGaussianBlur parseFilterElementGaussianBlur(final Attributes pAttributes) {
        final float standardDeviation = SAXHelper.getFloatAttribute(pAttributes, ATTRIBUTE_FILTER_ELEMENT_FEGAUSSIANBLUR_STANDARDDEVIATION);
        return new SVGFilterElementGaussianBlur(standardDeviation);
    }

    // ===========================================================
    // Inner and Anonymous Classes
    // ===========================================================
}
